﻿using System;
using System.Collections.Generic;
using System.Threading;

using OpenTK;
using OpenTK.Graphics.OpenGL;

using GLSL;

using MosCraft.Interfaces;
using MosCraft.Utilities;
using MosCraft.Terrain;

namespace MosCraft.Graphics
{
    public class WorldRender
    {
        private World World;
        private GLSLShader SolidShader;
        private GLSLShader BlendShader;
        private Entity Player;
        private ChunkMeshBuilder chunkMesher;
        private ChunkRender chunkRender;
        private IChunkGenerator chunkProvider;
        private byte GENERATE_RANGE;
        private byte POPULATE_RANGE;
        private byte BUILD_RANGE;
        private byte REMOVE_RANGE;
        private int FOGNEAR;
        private int FOGFAR;

        #region Threading
        private Thread buildThread;
        private Thread terrainThread;
        private Queue<ChunkCoordinate> buildMeshQueue;
        private Queue<ChunkCoordinate> terrainQueue;
        private bool runThreads;       
        #endregion

        public WorldRender(World World)
        {
            this.SetRanges(4);

            this.World = World;
            this.chunkMesher = new ChunkMeshBuilder(World);
            this.chunkRender = new ChunkRender();
            this.runThreads = true;
            this.buildMeshQueue = new Queue<ChunkCoordinate>();
            this.terrainQueue = new Queue<ChunkCoordinate>();
            this.chunkProvider = World.worldProvider.getChunkProvider();
            
            #region Setup Threads for Meshes and Terrain
            buildThread = new Thread(new ThreadStart(BuildChunkMesh))
            {
                Priority = ThreadPriority.Highest,
                IsBackground = true
            };
            buildThread.Start();

            terrainThread = new Thread(new ThreadStart(ChunkTerrainGenerate))
            {
                Priority = ThreadPriority.Highest,
                IsBackground = true
            };
            terrainThread.Start();
            #endregion

            #region Setup GSLS Shaders
            SolidShader = new GLSLShader()
            {
                VertexShader = GLSLShader.Create(ShaderType.VertexShader, StaticShader.VertSource),
                FragShader = GLSLShader.Create(ShaderType.FragmentShader, StaticShader.FragSource)
            };
            SolidShader.Complie();

            BlendShader = new GLSLShader()
            {
                VertexShader = GLSLShader.Create(ShaderType.VertexShader, StaticShader.VertSource),
                FragShader = GLSLShader.Create(ShaderType.FragmentShader, StaticShader.FragAlphaSource)
            };
            BlendShader.Complie();
            #endregion
        }

        public void SetRanges(byte build)
        {
            BUILD_RANGE = build;
            POPULATE_RANGE = (byte)(BUILD_RANGE + 1);
            GENERATE_RANGE = (byte)(POPULATE_RANGE + 1);
            REMOVE_RANGE = (byte)(GENERATE_RANGE + 1);

            FOGNEAR = (BUILD_RANGE - 1) * 16;
            FOGFAR = (BUILD_RANGE + 1) * 16;
        }

        #region Threaded Functions
        public void QueueBuild(ChunkCoordinate chunkCoord)
        {
            lock (buildMeshQueue)
            {
                buildMeshQueue.Enqueue(chunkCoord);
            }
        }

        private void BuildChunkMesh()
        {
            bool isBuilding = false;
            ChunkCoordinate chunkIndex = null;
            
            while (runThreads)
            {
                //LOOK FOR CHUNKS THAT NEED MESH
                lock (buildMeshQueue)
                {
                    if (buildMeshQueue.Count > 0)
                    {
                        chunkIndex = buildMeshQueue.Dequeue();
                        isBuilding = true;
                    }
                }

                if (isBuilding)
                {
                    try
                    {
                        Chunk chunkBuild = World.Chunks[chunkIndex.X, chunkIndex.Z];
                        if (chunkBuild.State == ChunkState.AwaitingBuild || chunkBuild.State == ChunkState.AwaitingRebuild)
                        {
                            DoBuild(chunkBuild);
                        }

                    }
                    catch
                    {
                        Console.WriteLine("NullReferenceException DoBuild target = {0}", chunkIndex.ToString());
                    }
                    continue;
                }

                Thread.Sleep(1);
            }
        }

        private void DoBuild(Chunk chunk)
        {
            lock (chunkMesher)
            {
                if (chunk != null)
                {
                    chunk.State = ChunkState.Building;
                    Console.WriteLine(" BUILDING MESH ({0}, {1})", chunk.xPosition, chunk.zPosition);
                    chunkMesher.BuildMesh(chunk);
                    chunk.State = ChunkState.needsVBO;
                } 
            }
        }

        public void QueueTerrain(ChunkCoordinate chunkCoord)
        {
            lock (terrainQueue)
            {
                terrainQueue.Enqueue(chunkCoord);
            }
        }

        private void ChunkTerrainGenerate()
        {
            bool isTerrainBuilding = false;
            ChunkCoordinate chunkIndex = null;

            while (runThreads)
            {
                //LOOK FOR CHUNKS THAT NEED MESH
                lock (terrainQueue)
                {
                    if (terrainQueue.Count > 0)
                    {
                        chunkIndex = terrainQueue.Dequeue();
                        isTerrainBuilding = true;
                    }
                }

                if (isTerrainBuilding)
                {
                    try
                    {
                        Chunk chunkTarget = World.Chunks[chunkIndex.X, chunkIndex.Z];

                        if (chunkTarget.State == ChunkState.AwaitingPopulation)
                        {
                            DoPopulate(chunkTarget);
                        }
                    }
                    catch
                    {
                        Console.WriteLine("NullReferenceException DoTerrain target = {0}", chunkIndex.ToString());
                    }
                    continue;
                }

                Thread.Sleep(1);
            }
        }

        private void DoPopulate(Chunk chunk)
        {
            lock (chunkProvider)
            {
                if (chunk != null)
                {
                    chunkProvider.Populate(chunk.xPosition, chunk.zPosition);
                    chunk.State = ChunkState.AwaitingBuild;
                }
            }
        }
        #endregion

        public void UpdateChunks(Entity Player)
        {
            this.Player = Player;
            ChunkCoordinate chunkOver = Player.getChunkOver();

            for (int x = chunkOver.X - REMOVE_RANGE; x < chunkOver.X + REMOVE_RANGE; x++)
            {
                for (int z = chunkOver.Z - REMOVE_RANGE; z < chunkOver.Z + REMOVE_RANGE; z++)
                {
                    Chunk chunk = World.Chunks[x, z];
                    ChunkCoordinate chunkIndex = new ChunkCoordinate(x, z);
                    int distancetoPlayer = chunkOver.DistanceToFloored(chunkIndex);

                    if (distancetoPlayer <= POPULATE_RANGE && POPULATE_RANGE > distancetoPlayer)
                    {
                        if (chunk != null && chunk.State == ChunkState.AwaitingPopulation)
                        {
                            QueueTerrain(chunkIndex);
                        }
                    }

                    if (distancetoPlayer <= BUILD_RANGE && BUILD_RANGE > distancetoPlayer)
                    {
                        if (chunk != null)
                        {
                            if (chunk.State == ChunkState.AwaitingRebuild || chunk.State == ChunkState.AwaitingBuild)
                            {
                                QueueBuild(chunkIndex);
                            }

                            // Now add the chunk vertices to the VBo buffer
                            // Must be done from this thread to stop context error
                            lock (chunk)
                            {
                                if (chunk.State == ChunkState.needsVBO)
                                {
                                    chunkRender.updateVBOData(chunk);
                                    chunk.State = ChunkState.Ready;
                                }
                            }
                        }
                    }

                    if (distancetoPlayer <= GENERATE_RANGE && GENERATE_RANGE > distancetoPlayer)
                    {
                        if (chunk == null)
                        {
                            chunk = World.Chunks.addChunk(x, z);
                            chunk.State = ChunkState.AwaitingGenerate;
                            chunkProvider.Generate(ref chunk);
                            chunk.State = ChunkState.AwaitingPopulation;
                        }
                    }
                }
            }

            List<ChunkCoordinate> chunkToDrop = new List<ChunkCoordinate>();

            foreach (Chunk chunk in World.Chunks.Values)
            {
                ChunkCoordinate chunkIndex = new ChunkCoordinate(chunk.xPosition, chunk.zPosition);
                int distancetoPlayer = chunkIndex.DistanceToFloored(chunkOver);

                Console.WriteLine();

                if (distancetoPlayer > (REMOVE_RANGE))
                {
                    chunkToDrop.Add(chunkIndex);
                }
            }

            foreach (ChunkCoordinate chunkindex in chunkToDrop)
            {
                Console.WriteLine("CHUNK DROPED {0}", chunkindex.ToString());
                World.Chunks.dropChunk(chunkindex.X, chunkindex.Z);
            }

            chunkToDrop.Clear();
        }

        public void RenderChunks()
        {
            ViewFrustum viewfrustum = Player.getFirstPersonCamera().getViewFrustum();

            GL.MatrixMode(MatrixMode.Modelview);
            GL.PushMatrix();

            GL.Enable(EnableCap.DepthTest);
            GL.Enable(EnableCap.CullFace);
            GL.Enable(EnableCap.Blend);
            GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha);

            foreach (Chunk chunk in World.Chunks.Values)
            {
                //Lock this chunk from other threads
                //while we call its VBO to render it.
                if (chunk != null && chunk.solidVBOHandler != null)
                {
                    lock (chunk)
                    {
                        if (viewfrustum.isBoundingBoxInFrustum(chunk.chunkBoundingBox))
                        {
                            //First pass for solid objects
                            SolidShader.BindShader();
                            SolidShader.UseTexture(Texture.getTexture("terrain.png"));
                            chunkRender.Render(chunk, RenderPass.Solids);
                            GLSLShader.Unbind();

                            //Second pass for translucent objects
                            BlendShader.BindShader();
                            BlendShader.UseTexture(Texture.getTexture("terrain.png"));
                            chunkRender.Render(chunk, RenderPass.Blended);
                            GLSLShader.Unbind();
                        }
                    }
                }
            }

            GL.Disable(EnableCap.DepthTest);
            GL.Disable(EnableCap.CullFace);
            GL.Disable(EnableCap.Blend);

            GL.PopMatrix();
        }

        public override string ToString()
        {
            return string.Format("{0} Generating {1} Building: {2}", World.Chunks.ToString(), terrainQueue.Count, buildMeshQueue.Count);
        }
    }
}
